import { useCallback, useMemo, useEffect } from 'react' import { useParams, useLocation, resolvePath, useNavigate, Link, } from 'react-router-dom' import { Field, Form, Formik, useFormikContext } from 'formik' import { DialogStateReturn } from 'reakit' import IVInputField from '~/components/IVInputField' import AsyncIVSelect from '~/components/IVSelect/async' import IVButton from '~/components/IVButton' import { inferQueryOutput, inferMutationInput, inferMutationOutput, trpc, client, } from '~/utils/trpc' import { notify } from '~/components/NotificationCenter' import IVDialog, { useDialogState } from '~/components/IVDialog' import PageHeading from '~/components/PageHeading' import UsersList from '~/components/UsersList' import IVSpinner from '~/components/IVSpinner' import NotFound from '~/components/NotFound' import SectionHeading from '~/components/SectionHeading' import { useHasPermission } from '~/components/DashboardContext' import useCopyToClipboard from '~/utils/useCopyToClipboard' import IconClipboard from '~/icons/compiled/Clipboard' import IVAlert from '~/components/IVAlert' import IconInfo from '~/icons/compiled/Info' import IVStatusPill from '~/components/IVStatusPill' function EditGroupDialog({ dialog, group, onSubmit, }: { dialog: DialogStateReturn group: inferQueryOutput<'group.one'> onSubmit?: (group: inferMutationOutput<'group.edit'>) => void }) { const editGroup = trpc.useMutation('group.edit') return ( ['data']> initialValues={{ name: group.name }} onSubmit={data => { editGroup.mutate( { id: group.id, data }, { onSuccess: onSubmit, } ) }} > {({ isValid }) => (
)}
) } function AddUserDialog({ groupId, dialog, existingUsers, onSubmit, }: { groupId: string dialog: DialogStateReturn existingUsers?: inferQueryOutput<'dashboard.users.index'>['users'] onSubmit?: () => void }) { const addUser = trpc.useMutation('group.users.add') const existingUserAccessIds: Set | undefined = useMemo(() => { return new Set(existingUsers?.map(access => access.id)) }, [existingUsers]) const { isSuccess } = addUser const { hide } = dialog useEffect(() => { if (isSuccess) { hide() notify.success('Member was added to the organization.') if (onSubmit) { onSubmit() } } }, [isSuccess, onSubmit, hide]) const isHidden = !dialog.visible && !dialog.animating const { reset: resetMutation } = addUser useEffect(() => { if (isHidden) resetMutation() }, [isHidden, resetMutation]) const handleSearch = useCallback( async (searchQuery: string) => { let accesses = await client.query('organization.users', { searchQuery, limit: 50, }) if (existingUserAccessIds) { accesses = accesses.filter( access => !existingUserAccessIds.has(access.id) ) } return accesses.map(({ id, user }) => { const name = [user.firstName, user.lastName].join(' ') return { value: id, label: name || user.email, } }) }, [existingUserAccessIds] ) return ( initialValues={{ userOrganizationAccessId: '', }} onSubmit={async ({ userOrganizationAccessId }) => { if (addUser.isLoading || !userOrganizationAccessId) return addUser.mutate({ userOrganizationAccessId, groupId, }) }} validate={({ userOrganizationAccessId }) => { if (!userOrganizationAccessId) { return { userOranizationAccessId: 'Please select a member.', } } }} > {({ setFieldValue, isValid }) => (
{ setFieldValue('userOrganizationAccessId', value) }} /> {addUser.isError && (

Sorry, there was a problem adding the member to the group.

)}
)}
) } function ResetFormToken({ isResetReady }: { isResetReady: boolean }) { const { resetForm } = useFormikContext() useEffect(() => { if (isResetReady) resetForm() }, [resetForm, isResetReady]) return null } function DeleteGroupDialog({ dialog, group, onSubmit, }: { dialog: DialogStateReturn group: inferQueryOutput<'group.one'> onSubmit: () => void }) { const deleteGroup = trpc.useMutation('group.delete') const { orgSlug } = useParams<{ orgSlug: string }>() return (

Are you sure you want to delete this team? This cannot be undone.

{group.actionAccesses.length > 0 && ( <>

The following actions have permissions configured for this team:

)}
{ deleteGroup.mutate( { id: group.id }, { onSuccess: onSubmit, } ) }} />
) } export default function GroupPage() { const canAddUsers = useHasPermission('WRITE_USERS') const canReadUsers = useHasPermission('READ_USERS') const navigate = useNavigate() const location = useLocation() const { groupId: id } = useParams<{ groupId: string }>() const groupId = id as string const group = trpc.useQuery(['group.one', { id: groupId }], { retry(retryCount, error) { if (error.data?.code === 'NOT_FOUND') return false return retryCount < 3 }, }) const users = trpc.useQuery(['dashboard.users.index', { groupId }]) const addUserDialog = useDialogState() const editGroupDialog = useDialogState() const deleteGroupDialog = useDialogState() const { onCopyClick: copySlug } = useCopyToClipboard({ successMessage: 'Copied slug to clipboard', }) if (group.isLoading) { return } if (!group.data) { return } const isScimGroup = !!group.data.scimGroupId return (
‹ Teams
{group.data?.name ?? 'Team'} {group.data?.name.startsWith('Dep:') && ( )} } actions={ !isScimGroup ? [ { label: 'Edit', onClick: editGroupDialog.show, theme: 'secondary', disabled: !canAddUsers, }, { label: 'Add member', theme: 'primary', onClick: addUserDialog.show, disabled: !canAddUsers, }, ] : [] } />

Use this slug when assigning permissions to teams{' '} via code .

{canReadUsers && (
{isScimGroup && (

This team is managed by an external identity provider. You can manage this team and its members in your identity provider.

)} {users.data?.users.length ? ( ) : (

This team does not have any members.

)}
)} {canAddUsers && !isScimGroup && (
)}
{ editGroupDialog.hide() notify.success('Team updated.') group.refetch() }} /> { notify.success('Team deleted') navigate(resolvePath('..', location.pathname).pathname) }} />
) }